/* vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4: */
/* Copyright (C) 2012 Zongyou Yao. All rights reserved.     */

#include <stdlib.h>
#include <string.h>
#include "assert.h"
#include "except.h"
#include "arena.h"

const except_t arena_newfailed = { "Arena Creation Failed" };
const except_t arena_failed = { "Arena Allocation Failed" };

#define THRESHOLD 10

struct arena_t {
    arena_t prev;
    char *avail;
    char *limit;
};

union align {
#ifdef MAXALIGN
    char pad[MAXALIGN];
#else
    int i;
    long l;
    long *lp;
    void *p;
    void (*fp)(void);
    float f;
    double d;
    long double ld;
#endif
};

union header {
    struct arena_t b;
    union align a;
};


static arena_t freechunks;
static int nfree;

arena_t arena_new(void)
{
    arena_t arena = malloc(sizeof(*arena));
    if (arena == NULL) {
        RAISE(arena_newfailed);
    }
    arena->prev = NULL;
    arena->limit = arena->avail = NULL;
    return arena;
}

void arena_dispose(arena_t *ap)
{
    assert(ap && *ap);
    arena_free(*ap);
    free(*ap);
    *ap = NULL;
}

void * arena_alloc(arena_t arena, long nbytes, const char *file, int line)
{
    assert(arena);
    assert(nbytes > 0);
    nbytes = ((nbytes + sizeof(union align) - 1) / (sizeof(union align))) * (sizeof(union align));
    while (nbytes > arena->limit - arena->avail) {
        arena_t ptr;
        char *limit;
        if ((ptr = freechunks) != NULL) {
            freechunks = freechunks->prev;
            nfree--;
            limit = ptr->limit;
        } else {
            long m = sizeof(union header) + nbytes + 10 * 1024;
            ptr = malloc(m);
            if (ptr == NULL) {
                if (file == NULL) {
                    RAISE(arena_failed);
                } else {
                    except_raise(&arena_failed, file, line);
                }
            }
            limit = (char *)ptr + m;
        }
        *ptr = *arena;
        arena->avail = (char *)((union header *)ptr + 1);
        arena->limit = limit;
        arena->prev = ptr;
    }
    arena->avail += nbytes;
    return arena->avail - nbytes;
}

void * arena_calloc(arena_t arena, long count, long nbytes, const char *file, int line)
{
    void *ptr;
    assert(count > 0);
    ptr = arena_alloc(arena, count * nbytes, file, line);
    memset(ptr, '\0', count * nbytes);
    return ptr;
}

void arena_free(arena_t arena)
{
    assert(arena);
    while (arena->prev) {
        struct arena_t tmp = *arena->prev;
        if (nfree < THRESHOLD) {
            arena->prev->prev = freechunks;
            freechunks = arena->prev;
            nfree++;
            freechunks->limit = arena->limit;
        } else {
            free(arena->prev);
        }
        *arena = tmp;
    }
    assert(arena->limit == NULL);
    assert(arena->avail == NULL);
}
